library oss_dart;

/// A Flutter package for Alibaba Cloud Object Storage Service (OSS) with null safety support.
/// Supports both STS authentication and direct AccessKey authentication.
export 'utils.dart';

import 'dart:async';
import 'dart:typed_data';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:http/http.dart' as http;

import 'utils.dart';

/// OSS Client V2 - Supports both STS and direct authentication
class OssClient {
  // Common properties
  final String endpoint;
  final String bucketName;
  final String? proxy;

  // STS authentication properties
  final Function? tokenGetter;
  String? _stsAccessKey;
  String? _stsAccessSecret;
  String? _stsSecureToken;
  String? _stsExpire;

  // Direct authentication properties
  final String? directAccessKey;
  final String? directAccessSecret;
  final String? directSecureToken;

  // Internal working variables
  Map<String, String> _headers = {};
  String? _url;
  String? _method;
  String? _fileKey;

  /// Create OSS Client with STS authentication
  OssClient.sts({
    required this.endpoint,
    this.bucketName = '',
    required this.tokenGetter,
    this.proxy,
  })  : directAccessKey = null,
        directAccessSecret = null,
        directSecureToken = null;

  /// Create OSS Client with direct AccessKey authentication
  OssClient.direct({
    required this.endpoint,
    this.bucketName = '',
    required this.directAccessKey,
    required this.directAccessSecret,
    this.directSecureToken,
    this.proxy,
  }) : tokenGetter = null;

  /// Legacy constructor for backward compatibility (STS mode)
  OssClient({
    String? endpoint,
    String bucketName = '',
    Function? tokenGetter,
    String? proxy,
  }) : this.sts(
          endpoint: endpoint ?? '',
          bucketName: bucketName,
          tokenGetter: tokenGetter,
          proxy: proxy,
        );

  bool _checkExpire(String? expire) {
    if (expire == null) {
      return true;
    }
    final expireIn = DateTime.parse(expire).add(const Duration(seconds: -10)).toLocal();
    return DateTime.now().compareTo(expireIn) > 0;
  }

  String _createXml(List<String> completeParts) {
    var xml = '<?xml version="1.0" encoding="UTF-8"?>\n<CompleteMultipartUpload>\n';
    for (var i = 0; i < completeParts.length; i++) {
      xml += '<Part>\n';
      xml += '<PartNumber>${i + 1}</PartNumber>\n';
      xml += '<ETag>${completeParts[i]}</ETag>\n';
      xml += '</Part>\n';
    }
    xml += '</CompleteMultipartUpload>';
    return xml;
  }

  /// Initialize authentication - handles both STS and direct auth
  Future<void> _initAuth() async {
    if (tokenGetter != null) {
      // STS authentication mode
      if (_stsExpire == null || _checkExpire(_stsExpire)) {
        Map<String, dynamic> stsInfo = await tokenGetter!();
        _stsAccessKey = stsInfo['AccessKeyId'] as String?;
        _stsAccessSecret = stsInfo['AccessKeySecret'] as String?;
        _stsSecureToken = stsInfo['SecurityToken'] as String?;
        _stsExpire = stsInfo['Expiration'] as String?;
      }
    } else if (directAccessKey != null && directAccessSecret != null) {
      // Direct authentication mode - no initialization needed
      return;
    } else {
      throw Exception('必须配置STS认证或直接认证方式');
    }
  }

  /// Get current access key (from STS or direct)
  String get _currentAccessKey {
    if (tokenGetter != null) {
      return _stsAccessKey ?? '';
    } else {
      return directAccessKey ?? '';
    }
  }

  /// Get current access secret (from STS or direct)
  String get _currentAccessSecret {
    if (tokenGetter != null) {
      return _stsAccessSecret ?? '';
    } else {
      return directAccessSecret ?? '';
    }
  }

  /// Get current security token (from STS or direct)
  String? get _currentSecureToken {
    if (tokenGetter != null) {
      return _stsSecureToken;
    } else {
      return directSecureToken;
    }
  }

  /// 上传文件 (内存数据)
  /// @param fileData type:List<int> data of upload file
  /// @param fileKey type:String upload filename
  /// @param header type:Map optional headers
  Future<http.Response> putObject(List<int> fileData, String fileKey,
      [Map<String, String>? header]) async {
    await _initAuth();
    _headers = {'content-md5': md5File(fileData), 'content-type': 'application/octet-stream'};
    if (header != null) {
      for (var key in header.keys) {
        _headers[key] = header[key]!;
      }
    }
    _method = 'PUT';
    _fileKey = fileKey;
    _signRequest();
    return await http
        .put(Uri.parse(_url!), headers: _headers, body: fileData)
        .timeout(const Duration(seconds: 60));
  }

  /// 上传本地文件 (V2新功能)
  /// @param filePath type:String file absolute path
  /// @param fileKey type:String upload filename
  Future<Response> uploadFile(String filePath, String fileKey) async {
    await _initAuth();
    _headers = {
      'content-md5': await md5FileFromPath(filePath),
      'content-type': 'application/octet-stream'
    };
    _method = 'PUT';
    _fileKey = fileKey;
    _signRequest();
    Response res = await Dio().put(_url!,
        options: Options(
          headers: _headers,
          responseType: ResponseType.plain,
        ),
        data: File(filePath).openRead());
    return res;
  }

  /// 删除文件 (V2新功能)
  /// @param fileKey type:String upload filename
  Future<Response> deleteObject(String fileKey) async {
    await _initAuth();
    _headers = {}; // 清空headers，DELETE请求不需要content-md5
    _method = 'DELETE';
    _fileKey = fileKey;
    _signRequest();
    Response res = await Dio().delete(
      _url!,
      options: Options(
        headers: _headers,
        responseType: ResponseType.plain,
      ),
    );
    return res;
  }

  //初始化分片上传
  // @param fileKey type:String upload filename
  Future<http.Response> initiateMultipartUpload(String fileKey) async {
    await _initAuth();
    _headers = {}; // 清空headers，POST请求不需要content-md5
    _method = 'POST';
    _fileKey = fileKey;
    _signRequest();
    return await http.post(Uri.parse(_url!), headers: _headers);
  }

  //上传分片
  // @param fileData type:List<int> data of upload file
  // @param uploadId type:String name of multiUpload
  // @param fileKey type:String upload filename
  Future<http.Response> uploadPart(
      List<int> fileData, String fileKey, String uploadId, num partNum) async {
    return putObject(fileData, '$fileKey?partNumber=$partNum&uploadId=$uploadId');
  }

  //完成分片上传
  // @param etags type:List 所有分片的tag，按上传编号排序
  // @param uploadId type:String name of multiUpload
  // @param fileKey type:String upload filename
  Future<http.Response> completeMultipartUpload(
      List<String> etags, String fileKey, String uploadId) async {
    await _initAuth();
    String xml = _createXml(etags);
    var bytes = Uint8List.fromList(xml.codeUnits);
    _headers = {'content-md5': md5File(bytes)};
    _method = 'POST';
    _fileKey = '$fileKey?uploadId=$uploadId';
    _signRequest();
    return await http.post(Uri.parse(_url!), headers: _headers, body: bytes);
  }

  //列举指定Upload ID所属的所有已经上传成功Part
  // @param uploadId type:String name of multiUpload
  // @param fileKey type:String upload filename
  Future<http.Response> listParts(String fileKey, String uploadId) async {
    await _initAuth();
    _headers = {}; // 清空headers，GET请求不需要content-md5
    _method = 'GET';
    _fileKey = '$fileKey?uploadId=$uploadId';
    _signRequest();
    return await http.get(Uri.parse(_url!), headers: _headers);
  }

  /// 检查文件是否存在
  /// @param fileKey type:String upload filename
  /// @return bool 文件是否存在
  Future<bool> objectExists(String fileKey) async {
    try {
      await _initAuth();
      _headers = {}; // 清空headers，HEAD请求不需要content-md5
      _method = 'HEAD';
      _fileKey = fileKey;
      _signRequest();
      final response = await http.head(Uri.parse(_url!), headers: _headers);
      return response.statusCode == 200;
    } on Exception {
      return false;
    }
  }

  /// 获取文件元数据信息
  /// @param fileKey type:String upload filename
  /// @return http.Response 包含文件元数据的响应
  Future<http.Response> getObjectMeta(String fileKey) async {
    await _initAuth();
    _headers = {}; // 清空headers，HEAD请求不需要content-md5
    _method = 'HEAD';
    _fileKey = fileKey;
    _signRequest();
    return await http.head(Uri.parse(_url!), headers: _headers);
  }

  //下载文件
  // @param fileKey type:String upload filename
  Future<http.Response> getObject(String fileKey) async {
    await _initAuth();
    _headers = {}; // 清空headers，GET请求不需要content-md5
    _method = 'GET';
    _fileKey = fileKey;
    _signRequest();
    return await http.get(Uri.parse(_url!), headers: _headers);
  }

  //下载大文件 (使用Dio支持进度回调)
  // @param fileKey type:String upload filename
  Future<Response> downloadObject(String fileKey, String savePath) async {
    await _initAuth();
    _headers = {}; // 清空headers，GET请求不需要content-md5
    _method = 'GET';
    _fileKey = fileKey;
    _signRequest();
    return await Dio().download(_url!, savePath, options: Options(headers: _headers));
  }

  //签名url
  void _signRequest() {
    if (proxy != null) {
      _url = "http://$proxy/$_fileKey";
    } else {
      _url = "http://$bucketName.$endpoint/$_fileKey";
    }
    _headers['date'] = httpDateNow();
    if (_currentSecureToken != null) {
      _headers['x-oss-security-token'] = _currentSecureToken!;
    }
    final String signature = _makeSignature();
    _headers['authorization'] = "OSS $_currentAccessKey:$signature";
  }

  String _makeSignature() {
    final String stringToSign = _getStringToSign();
    return hmacSign(_currentAccessSecret, stringToSign);
  }

  String _getStringToSign() {
    final String resourceString = _getResourceString();
    final String headersString = _getHeadersString();
    final String contentMd5 = _headers['content-md5'] ?? '';
    final String contentType = _headers['content-type'] ?? '';
    final String date = _headers['date'] ?? '';
    return "$_method\n$contentMd5\n$contentType\n$date\n$headersString$resourceString";
  }

  String _getResourceString() {
    if (bucketName.isEmpty) {
      return "/";
    } else {
      return "/$bucketName/$_fileKey";
    }
  }

  String _getHeadersString() {
    final List<String> canonHeaders = <String>[];
    for (final String key in _headers.keys) {
      if (key.toLowerCase().startsWith('x-oss-')) {
        canonHeaders.add(key.toLowerCase());
      }
    }
    canonHeaders.sort();

    if (canonHeaders.isNotEmpty) {
      final List<String> headerStrings = canonHeaders.map((String key) {
        final dynamic v = _headers[key];
        return "$key:$v";
      }).toList();
      return "${headerStrings.join('\n')}\n";
    } else {
      return '';
    }
  }
}
